//
//  ScriptsInvoker.swift
//  MacDemo
//
//  Created by FH on 2020/12/29.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

// (forEncode: Bool, typeName: String, value: Any) -> string for encode, real value for decode
typealias ConvertCustomTypeValueHandler = (Bool, String, Any) -> Any?
// context.state.key -> initValue
typealias ContextStatesInitValueHandler = (String) -> Any?
fileprivate var _handleCustomTypeValueFn: ConvertCustomTypeValueHandler? = nil

// REMARK: help methods

// format custom value str
func getCustomStateValue(_ value: Any?) -> String {
    let (valueType, valueStr) = getAnyValueStr(from: value)
    return "custom^*^\(valueType)^*^\(valueStr)"
}

func parseCustomStateValue(_ customStateStr: String) -> Any?? {
    if customStateStr.starts(with: "custom") {
        let split = customStateStr.components(separatedBy: "^*^")
        if split.count == 3 {
            let valueType = split[1]
            let valueStr = split[2]
            return getAnyValue(by: valueType, and: valueStr)
        }
    }
    return nil
}

// format cell mapping streamId str
func getCellMappingStreamId(_ argName: String, _ streamId: UInt) -> String {
    if argName.count > 0 {
        return "cellStreamId^*^\(argName)"
    } else {
        return "cellStreamId^*^\(streamId)"
    }
}

func getCellActionName(_ actionName: String, _ streamId: UInt) -> String {
    return "\(actionName)^*^\(streamId)"
}

func parseCellMappingStreamId(_ mappingStreamIdStr: String, _ argValues: [String : Any]) -> UInt? {
    if mappingStreamIdStr == "" {
        for (key, arg) in argValues {
            if key == "__ctx", let items = arg as? [String : Any] {
                for (ctxKey, _) in items {
                    if let streamId = parseCellMappingStreamId(ctxKey, argValues) {
                        return streamId
                    }
                }
                break
            }
        }
    } else if mappingStreamIdStr.starts(with: "cellStreamId") {
        let split = mappingStreamIdStr.components(separatedBy: "^*^")
        if split.count == 2 {
            let argName = split[1]
            if let streamId = UInt(split[1]) {
                return streamId
            } else if let streamId = argValues[argName] as? UInt {
                return streamId
            }
        }
    }
    return nil
}

func parseCellActionName(_ actionName: String) -> (String, String, UInt)? {
    let split = actionName.components(separatedBy: "^*^")
    if split.count == 2, let streamId = UInt(split[1]), streamId > 0 {
        return (split[0], getCellMappingStreamId("", streamId), streamId)
    }
    return nil
}

// get type string for encode
func getAnyValueType(_ value: Any?) -> String {
    if let value = value {
        return String(reflecting: type(of: value))
    } else {
        return ""
    }
}

// get value string for encode -> (ValueType, ValueStr)
func getAnyValueStr(from value: Any?) -> (String, String) {
    var valueType = getAnyValueType(value)
    var valueStr = "nil"
    if var value = value {
        // handle Optional<Type>
        let (isOptional, realTypeStr) = parseOptionalType(type(of: value))
        if isOptional {
            if realTypeStr == "String", let realValue = value as? String {
                valueType = getAnyValueType(realValue)
                value = realValue
            } else if realTypeStr == "Bool", let realValue = value as? Bool {
                valueType = getAnyValueType(realValue)
                value = realValue
            } else if realTypeStr == "Int", let realValue = value as? Int {
                valueType = getAnyValueType(realValue)
                value = realValue
            } else if realTypeStr == "UInt", let realValue = value as? UInt {
                valueType = getAnyValueType(realValue)
                value = realValue
            } else if realTypeStr == "Float", let realValue = value as? Float {
                valueType = getAnyValueType(realValue)
                value = realValue
            } else if realTypeStr == "Double", let realValue = value as? Double {
                valueType = getAnyValueType(realValue)
                value = realValue
            }
        }
        if let handler = _handleCustomTypeValueFn, let result = handler(true, valueType, value) {
            valueStr = "\(result)"
        }
    }
    return (valueType, valueStr)
}

// get real value for decode
func getAnyValue(by valueType: String, and value: String) -> Any? {
    if value != "nil" {
        if valueType == "Swift.Bool" {
            return Bool(value)
        } else if valueType == "Swift.Int" {
            return Int(value)
        } else if valueType == "Swift.UInt" {
            return UInt(value)
        } else if valueType == "Swift.Float" {
            return Float(value)
        } else if valueType == "Swift.Double" {
            return Double(value)
        } else if valueType == "Swift.String" {
            return String(value)
        } else if let handler = _handleCustomTypeValueFn, let result = handler(false, valueType, value) {
            return result
        }
        
        fatalError("not support type [\(valueType)], value: \(value)")
    }
    return nil
}

// REMARK: Encoder & Expression

enum StateMappingControlType : Int {
    case none = 0
    case checkbox = 1
    case dropbox = 2
    case inputField = 3
    case inputFile = 4
    case inputBinary = 5
}

class StateMappingControl {
    let type: StateMappingControlType
    let aliasName: String
    let enumClass: String?
    let defaultSelectItem: String?
    let stringItems: [String]?
    let intRange: IntRange?
    var nonnull = false
    // only use to pass data
    var associatedActionArguments: [String : ScriptDirective]?
    
    init() {
        self.type = .none
        self.aliasName = ""
        self.intRange = nil
        self.enumClass = nil
        self.stringItems = nil
        self.defaultSelectItem = nil
    }
    
    init(_ type: StateMappingControlType, _ aliasName: String,
         enumClass: String?=nil, defaultSelectItem: String?=nil,
         intRange: IntRange?=nil, stringItems: [String]?=nil) {
        self.type = type
        self.aliasName = aliasName
        self.intRange = intRange
        self.enumClass = enumClass
        self.stringItems = stringItems
        self.defaultSelectItem = defaultSelectItem
    }
    
    init?(encoder: String) {
        let split = encoder.components(separatedBy: "-")
        guard split.count == 7 else {
            return nil
        }
        let intRangeStr = split[0]
        let enumClassStr = split[1]
        let stringItemsStr = split[2]
        let defaultSelectItem = split[3]
        let typeStr = split[4]
        let aliasName = split[5]
        let nonnull = split[6]
        if intRangeStr == "nil" {
            self.intRange = nil
        } else {
            self.intRange = encodeIntRange(with: intRangeStr)
        }
        if enumClassStr == "nil" {
            self.enumClass = nil
        } else {
            self.enumClass = enumClassStr
        }
        if stringItemsStr == "nil" {
            self.stringItems = nil
        } else {
            let split = stringItemsStr.components(separatedBy: ",")
            var items = [String]()
            for item in split {
                items.append(item)
            }
            self.stringItems = items
        }
        if defaultSelectItem == "nil" {
            self.defaultSelectItem = nil
        } else {
            self.defaultSelectItem = defaultSelectItem
        }
        if let type = Int(typeStr) {
            self.type = StateMappingControlType.init(rawValue: type)!
        } else {
            self.type = .none
        }
        self.aliasName = aliasName
        if let flag = Bool(nonnull) {
            self.nonnull = flag
        }
    }
    
    func decode() -> String {
        let type = self.type.rawValue
        let intRange = (self.intRange == nil) ? "nil" : "\(self.intRange!.decode())"
        let enumClass = self.enumClass ?? "nil"
        let stringItems = (self.stringItems == nil) ? "nil" : self.stringItems!.joined(separator: ",")
        let defaultSelectItem = self.defaultSelectItem ?? "nil"
        return "\(intRange)-\(enumClass)-\(stringItems)-\(defaultSelectItem)-\(type)-\(self.aliasName)-\(self.nonnull)"
    }
}

extension StateMappingControl : Equatable {
    static func == (lhs: StateMappingControl, rhs: StateMappingControl) -> Bool {
        var rangeEquals = false
        if lhs.intRange == nil && rhs.intRange == nil {
            rangeEquals = true
        } else if let lhsRange = lhs.intRange, let rhsRange = rhs.intRange {
            rangeEquals = lhsRange.equals(rhs: rhsRange)
        }
        
        return lhs.type == rhs.type
                && lhs.aliasName == rhs.aliasName
                && lhs.enumClass == rhs.enumClass
                && lhs.defaultSelectItem == rhs.defaultSelectItem
                && lhs.stringItems == rhs.stringItems
                && rangeEquals
    }
}

struct ScriptEncoder: Codable {
    let index: Int
    let value: String?
    let valueType: String
    let keyPath: String?
    var extendInfo: String? = nil
    
    static func register(_ customTypeValueHandler: @escaping ConvertCustomTypeValueHandler) {
        if _handleCustomTypeValueFn == nil {
            _handleCustomTypeValueFn = customTypeValueHandler
        }
    }
    
    func toDirective() -> ScriptDirective {
        switch index {
        case -1:
            return .idle
        case 1:
            return .breakPoint
        case 2:
            return .schedule(runInMainThread: Bool(value ?? "") ?? true)
        case 3:
            return .delay(interval: TimeInterval(value ?? "") ?? 0)
        case 4:
            return .iterate(times: Int(value ?? "") ?? 0)
        case 5:
            var mapping: OrderedDictionary<String, String> = [:]
            if let extendInfo = self.extendInfo, let data = extendInfo.data(using: .utf8),
               let dict = try? JSONDecoder().decode(OrderedDictionary<String, String>.self, from: data) {
                mapping = dict
            }
            let tupleValue = getTupleValue(value ?? "")
            return .waitCallback(selector: tupleValue.0,
                                 blockor: tupleValue.1 == "nil" ? nil : tupleValue.1,
                                 mapping: mapping)
        case 6:
            var mapping: OrderedDictionary<String, String> = [:]
            if let extendInfo = self.extendInfo, let data = extendInfo.data(using: .utf8),
               let dict = try? JSONDecoder().decode(OrderedDictionary<String, String>.self, from: data) {
                mapping = dict
            }
            let tupleValue = getTupleValue(value ?? "")
            return .subscribeCallback(selector: tupleValue.0,
                                      blockor: tupleValue.1 == "nil" ? nil : tupleValue.1,
                                      mapping: mapping)
        case 7:
            var mapping: OrderedDictionary<String, String> = [:]
            if let extendInfo = self.extendInfo, let data = extendInfo.data(using: .utf8),
               let dict = try? JSONDecoder().decode(OrderedDictionary<String, String>.self, from: data) {
                mapping = dict
            }
            return .waitCallAPI(mapping: mapping)
        case 8:
            var mappingControl: StateMappingControl? = nil
            if let extendInfo = self.extendInfo {
                mappingControl = StateMappingControl(encoder: extendInfo)
            }
            var handledKeyPath = self.keyPath ?? ""
            var compareOp = "="
            if let keyPath = self.keyPath {
                let split = keyPath.components(separatedBy: "^*^")
                if split.count == 2 {
                    handledKeyPath = split[1]
                    compareOp = split[0]
                }
            }
            return .predicateContext(keyPath: handledKeyPath, compareOp: compareOp,
                                     value: getAnyValue(by: valueType, and: value ?? ""),
                                     control: mappingControl)
        case 9:
            return .predicateResponds(methodName: self.keyPath ?? "", className: self.value ?? "", compareOp: self.extendInfo ?? "=")
        case 10:
            var mappingControl: StateMappingControl? = nil
            if let extendInfo = self.extendInfo {
                mappingControl = StateMappingControl(encoder: extendInfo)
            }
            return .contextStates(keyPath: keyPath ?? "", initValue: getAnyValue(by: valueType, and: value ?? ""), control: mappingControl)
        case 11:
            var args = [String : String]()
            if let extendInfo = self.extendInfo, let dict = extendInfo.toJSONObject() as? [String : String] {
                args = dict
            }
            return .contextCallAction(methodName: self.keyPath ?? "", args: args)
        case 12:
            return .contextArgToken
        case 13:
            return .preOperatorInstance
        default:
            return .idle
        }
    }
    
    func toAnyValue() -> Any? {
        return getAnyValue(by: valueType, and: value ?? "")
    }
    
    private func getTupleValue(_ value: String) -> (String, String) {
        let match = Tuple2Regex.findFirst(in: value)
        return (match?.group(named: "item1") ?? "", match?.group(named: "item2") ?? "")
    }
}

// REMARK: enums

enum ScriptDirective : Codable {
    private enum CodingKeys: String, CodingKey {
        case directive
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let encoder = try values.decode(ScriptEncoder.self, forKey: .directive)
        self = encoder.toDirective()
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var scriptEncoder: ScriptEncoder! = nil
        
        switch self {
        case .idle:
            scriptEncoder = ScriptEncoder(index: -1, value: nil, valueType: "", keyPath: nil)
        case .breakPoint:
            scriptEncoder = ScriptEncoder(index: 1, value: nil, valueType: "", keyPath: nil)
        case .schedule(let runInMainThread):
            scriptEncoder = ScriptEncoder(index: 2, value: "\(runInMainThread)", valueType: "Bool", keyPath: nil)
        case .delay(let interval):
            scriptEncoder = ScriptEncoder(index: 3, value: "\(interval)", valueType: "TimeInterval", keyPath: nil)
        case .iterate(let times):
            scriptEncoder = ScriptEncoder(index: 4, value: "\(times)", valueType: "Int", keyPath: nil)
        case .waitCallback(let selector, let blockor, let mapping):
            scriptEncoder = ScriptEncoder(index: 5,
                                          value: "(\(selector), \(blockor ?? "nil"))",
                                          valueType: "(String, String?)",
                                          keyPath: nil)
            if let data = try? JSONEncoder().encode(mapping), let json = String(data: data, encoding: .utf8) {
                scriptEncoder.extendInfo = json
            }
        case .subscribeCallback(let selector, let blockor, let mapping):
            scriptEncoder = ScriptEncoder(index: 6,
                                          value: "(\(selector), \(blockor ?? "nil"))",
                                          valueType: "(String, Bool)",
                                          keyPath: nil)
            if let data = try? JSONEncoder().encode(mapping), let json = String(data: data, encoding: .utf8) {
                scriptEncoder.extendInfo = json
            }
        case .waitCallAPI(let mapping):
            scriptEncoder = ScriptEncoder(index: 7, value: nil, valueType: "", keyPath: nil)
            if let data = try? JSONEncoder().encode(mapping), let json = String(data: data, encoding: .utf8) {
                scriptEncoder.extendInfo = json
            }
        case .predicateContext(let keyPath, let compareOp, let value, let control):
            let (valueType, valueStr) = getAnyValueStr(from: value)
            let handledKeyPath = compareOp == "=" ? keyPath : "\(compareOp)^*^\(keyPath)"
            scriptEncoder = ScriptEncoder(index: 8, value: valueStr, valueType: valueType, keyPath: handledKeyPath)
            if let control = control {
                scriptEncoder.extendInfo = control.decode()
            }
        case .predicateResponds(let methodName, let className, let compareOp):
            scriptEncoder = ScriptEncoder(index: 9, value: className, valueType: "", keyPath: methodName, extendInfo: compareOp)
        case .contextStates(let keyPath, let initValue, let control):
            let (valueType, valueStr) = getAnyValueStr(from: initValue)
            scriptEncoder = ScriptEncoder(index: 10, value: valueStr, valueType: valueType, keyPath: keyPath)
            if let control = control {
                scriptEncoder.extendInfo = control.decode()
            }
        case .contextCallAction(let methodName, let args):
            scriptEncoder = ScriptEncoder(index: 11, value: nil, valueType: "", keyPath: methodName)
            scriptEncoder.extendInfo = args.formatJSONString()
        case .contextArgToken:
            scriptEncoder = ScriptEncoder(index: 12, value: nil, valueType: "", keyPath: nil)
        case .preOperatorInstance:
            scriptEncoder = ScriptEncoder(index: 13, value: nil, valueType: "", keyPath: nil)
        }
        try container.encode(scriptEncoder, forKey: .directive)
    }
    
    case idle
    // pause running script, it only for semi-automatic mode
    case breakPoint
    // run operator on main thread or background
    case schedule(runInMainThread: Bool)
    case delay(interval: TimeInterval)
    case iterate(times: Int)
    // wait operator(api) dependency block or delegate callback finish, like: joinChannelByToken:channelId:info:uid:joinSuccess:
    case waitCallback(selector: String, blockor: String?, mapping: OrderedDictionary<String, String>)
    // non-main bundle instructions subscribe block or delegate callback to do something. like render canvas in didJoinedOfUid:
    case subscribeCallback(selector: String, blockor: String?, mapping: OrderedDictionary<String, String>)
    
    // REMARK: belong To Operator
    
    // match args/return names and set value to context states in responseAPI
    case waitCallAPI(mapping: OrderedDictionary<String, String>)
    // detect context's states match to given value and compareOp
    case predicateContext(keyPath: String, compareOp: String, value: Any?, control: StateMappingControl?)
    // detect sdk' method can be invoked or not
    case predicateResponds(methodName: String, className: String, compareOp: String)
    // get context states
    case contextStates(keyPath: String, initValue: Any?, control: StateMappingControl?)
    // call action in appContext
    case contextCallAction(methodName: String, args: [String : String])
    // get token async result from context
    case contextArgToken
    // use pre operator's instance as arg
    case preOperatorInstance
}

extension ScriptDirective : Equatable {
    func isKind(of rhs: ScriptDirective) -> Bool {
        let result = (self, rhs)
        switch result {
        case (.schedule, .schedule)
             , (.delay, .delay)
             , (.iterate, .iterate)
             , (.contextStates, .contextStates)
             , (.waitCallback, .waitCallback)
             , (.waitCallAPI, .waitCallAPI)
             , (.subscribeCallback, .subscribeCallback)
             , (.predicateContext, .predicateContext)
             , (.predicateResponds, .predicateResponds)
             , (.contextCallAction, .contextCallAction):
            return true
        default:
            return self == rhs
        }
    }
    
    static func == (lhs: ScriptDirective, rhs: ScriptDirective) -> Bool {
        let result = (lhs, rhs)
        switch result {
        case (.idle, .idle)
            , (.breakPoint, .breakPoint)
            , (.contextArgToken, .contextArgToken)
            , (.preOperatorInstance, .preOperatorInstance):
            return true
        case (.schedule(let lhsValue), .schedule(let rhsValue)):
            return lhsValue == rhsValue
        case (.delay(let lhsValue), .delay(let rhsValue)):
            return lhsValue == rhsValue
        case (.iterate(let lhsValue), .iterate(let rhsValue)):
            return lhsValue == rhsValue
        case (.waitCallAPI(let lhsMapping), .waitCallAPI(let rhsMapping)):
            return lhsMapping == rhsMapping
        case (.waitCallback(let lhsValue1, let lhsValue2, let lhsMapping),
              .waitCallback(let rhsValue1, let rhsValue2, let rhsMapping)):
            return lhsValue1 == rhsValue1 && lhsValue2 == rhsValue2 && lhsMapping == rhsMapping
        case (.contextStates(let lhskeyPath, let lhsValue, let lhsControl),
              .contextStates(let rhskeyPath, let rhsValue, let rhsControl)):
            return lhskeyPath == rhskeyPath && equalsOptionalAny(lhs: lhsValue, rhs: rhsValue)
                && lhsControl == rhsControl
        case (.subscribeCallback(let lhsValue1, let lhsValue2, let lhsMapping),
              .subscribeCallback(let rhsValue1, let rhsValue2, let rhsMapping)):
            return lhsValue1 == rhsValue1 && lhsValue2 == rhsValue2 && lhsMapping == rhsMapping
        case (.predicateContext(let lhsKeyPath, let lhsCompareOp, let lhsValue, let lhsControl),
              .predicateContext(let rhsKeyPath, let rhsCompareOp, let rhsValue, let rhsControl)):
            return lhsKeyPath == rhsKeyPath && lhsCompareOp == rhsCompareOp
                && equalsOptionalAny(lhs: lhsValue, rhs: rhsValue)
                && lhsControl == rhsControl
        case (.predicateResponds(let lhsMethodName, let lhsClassName, let lhsCompareOp),
              .predicateResponds(let rhsMethodName, let rhsClassName, let rhsCompareOp)):
            return lhsMethodName == rhsMethodName && lhsClassName == rhsClassName && lhsCompareOp == rhsCompareOp
        case (.contextCallAction(let lhsMethodName, let lhsArgs),
              .contextCallAction(let rhsMethodName, let rhsArgs)):
            return lhsMethodName == rhsMethodName && lhsArgs.equals(rhs: rhsArgs)
        default:
            return false
        }
    }
}

enum ScriptsInvokeMode : Int, Codable {
    case automatic = 0
    case semiAutomatic = 1
    case manual = 2
}

enum ScriptsRunningMode : String, Codable {
    case notStart = "notStart"
    case running = "running"
    case pause = "pause"
}

enum ScriptInstructionCategory : Int, Codable {
    case none
    case sdk
    case cppSDK
    case cocoa
    case appContext // TODO: remove
    case appUILayout
}

enum ScriptInstructionStage : Int, Codable {
    case createInstance
    case configSettings
    case startRunning
    case exitRunning
    case destoryInstance
    
    init?(strValue: String) {
        var rawValue = -1
        if strValue == "createInstance" {
            rawValue = 0
        } else if strValue == "configSettings" {
            rawValue = 1
        } else if strValue == "startRunning" {
            rawValue = 2
        } else if strValue == "exitRunning" {
            rawValue = 3
        } else if strValue == "destoryInstance" {
            rawValue = 4
        } else {
            return nil
        }
        self.init(rawValue: rawValue)
    }
}

// REMARK: Operator & Instruction

class ScriptOperator : Copyable, Codable {
    let forCreateInstance: Bool
    let targetClass: String!
    let group: String?
    var selector: String!
    fileprivate (set) var args: [String : Any?]? = nil
    fileprivate (set) var directiveArgs: [String : ScriptDirective]? = nil
    // .waitCallAPI and contextCallAction can be grouped, .predicateContext and .predicateResponds can be grouped
    fileprivate (set) var waitCallAPIDirective: ScriptDirective? = nil
    fileprivate (set) var contextCallActionDirective: ScriptDirective? = nil
    fileprivate (set) var predicateContextDirective: ScriptDirective? = nil
    fileprivate (set) var predicateRespondsDirective: ScriptDirective? = nil
    
    private enum CodingKeys: String, CodingKey {
        case forCreateInstance
        case targetClass
        case selector
        case group
        case properties
        case args
        case directiveArgs
        case directiveWaitCallAPI
        case contextCallAction
        case directivePredicate
        case directiveResponds
    }
    
    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        forCreateInstance = try values.decode(Bool.self, forKey: .forCreateInstance)
        targetClass = try values.decode(String?.self, forKey: .targetClass)
        selector = try values.decode(String?.self, forKey: .selector)
        group = try values.decode(String?.self, forKey: .group)
        directiveArgs = try values.decode([String : ScriptDirective]?.self, forKey: .directiveArgs)
        if let argEncoders = try values.decode([String : ScriptEncoder]?.self, forKey: .args) {
            var args = [String : Any?]()
            for (key, encoder) in argEncoders {
                args[key] = encoder.toAnyValue()
            }
            self.args = args
        }
        waitCallAPIDirective = try values.decode(ScriptDirective?.self, forKey: .directiveWaitCallAPI)
        contextCallActionDirective = try values.decode(ScriptDirective?.self, forKey: .contextCallAction)
        predicateContextDirective = try values.decode(ScriptDirective?.self, forKey: .directivePredicate)
        predicateRespondsDirective = try values.decode(ScriptDirective?.self, forKey: .directiveResponds)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(forCreateInstance, forKey: .forCreateInstance)
        try container.encode(targetClass, forKey: .targetClass)
        try container.encode(selector, forKey: .selector)
        try container.encode(group, forKey: .group)
        try container.encode(directiveArgs, forKey: .directiveArgs)
        try container.encode(waitCallAPIDirective, forKey: .directiveWaitCallAPI)
        try container.encode(contextCallActionDirective, forKey: .contextCallAction)
        try container.encode(predicateContextDirective, forKey: .directivePredicate)
        try container.encode(predicateRespondsDirective, forKey: .directiveResponds)
        var argEncoders: [String : ScriptEncoder]! = nil
        if let args = self.args {
            argEncoders = [:]
            for (key, value) in args {
                let (valueType, valueStr) = getAnyValueStr(from: value)
                argEncoders[key] = ScriptEncoder(index: 0, value: valueStr, valueType: valueType, keyPath: nil)
            }
        }
        try container.encode(argEncoders, forKey: .args)
    }
    
    // for copyable
    required init(instance: ScriptOperator) {
        self.forCreateInstance = instance.forCreateInstance
        self.targetClass = instance.targetClass
        self.selector = instance.selector
        self.group = instance.group
        self.args = instance.args
        self.directiveArgs = instance.directiveArgs
        self.waitCallAPIDirective = instance.waitCallAPIDirective
        self.contextCallActionDirective = instance.contextCallActionDirective
        self.predicateContextDirective = instance.predicateContextDirective
        self.predicateRespondsDirective = instance.predicateRespondsDirective
    }
    
    // for call sdk api
    init(selector: String, group: String?, targetClass: String?=nil, forCreateInstance: Bool=false) {
        self.forCreateInstance = forCreateInstance
        self.targetClass = targetClass
        self.selector = selector
        self.group = group
    }
    
    // for invoke directives only
    init() {
        self.forCreateInstance = false
        self.targetClass = nil
        self.selector = nil
        self.group = nil
    }
    
    var forInvokeDirectives: Bool {
        if let directiveArgs = self.directiveArgs, directiveArgs.count > 0 {
            return self.targetClass.isEmpty && self.selector.isEmpty
        } else {
            return self.contextCallActionDirective != nil
        }
    }
    
    var forSetProperties: Bool {
        var hasArgs = false
        if let args = self.args, args.count > 0 {
            hasArgs = true
        } else if let args = self.directiveArgs, args.count > 0 {
            hasArgs = true
        }
        return (self.selector.isEmpty || self.forCreateInstance) && !self.targetClass.isEmpty && hasArgs
    }
    
    var forCallAPIs: Bool {
        return !self.selector.isEmpty
    }
    
    func addArgValue(_ argName: String, _ argValue: Any?) {
        if self.args == nil {
            self.args = [:]
        }
        self.args![argName] = argValue
    }
    
    func addDirectiveArg(_ argName: String, _ directive: ScriptDirective) {
        if self.directiveArgs == nil {
            self.directiveArgs = [:]
        }
        self.directiveArgs![argName] = directive
    }
    
    func cleanArgs() {
        self.args = nil
        self.directiveArgs = nil
    }
    
    func addCallActionDirectives(_ waitCallAPI: ScriptDirective?, _ contextCallAction: ScriptDirective?) {
        if self.waitCallAPIDirective == nil && waitCallAPI != nil {
            self.waitCallAPIDirective = waitCallAPI
        }
        if self.contextCallActionDirective == nil && contextCallAction != nil {
            self.contextCallActionDirective = contextCallAction
        }
    }
    
    func addPredicateDirectives(_ predicateContext: ScriptDirective?, _ predicateResponds: ScriptDirective?) {
        if self.predicateContextDirective == nil && predicateContext != nil {
            self.predicateContextDirective = predicateContext
        }
        if self.predicateRespondsDirective == nil && predicateResponds != nil {
            self.predicateRespondsDirective = predicateResponds
        }
    }

    fileprivate func updateDirectives(with other: ScriptOperator) {
        if self.waitCallAPIDirective == nil && other.waitCallAPIDirective != nil {
            self.waitCallAPIDirective = other.waitCallAPIDirective
        }
        if self.contextCallActionDirective == nil && other.contextCallActionDirective != nil {
            self.contextCallActionDirective = other.contextCallActionDirective
        }
        if self.predicateContextDirective == nil && other.predicateContextDirective != nil {
            self.predicateContextDirective = other.predicateContextDirective
        }
        if self.predicateRespondsDirective == nil && other.predicateRespondsDirective != nil {
            self.predicateContextDirective = other.predicateContextDirective
        }
        if let otherArgs = other.directiveArgs {
            let currArgs = self.directiveArgs ?? [:]
            for (key, directive) in otherArgs {
                if currArgs[key] == nil {
                    self.addDirectiveArg(key, directive)
                }
            }
        }
    }
}

extension ScriptOperator : Hashable {
    func isKind(of rhs: ScriptOperator) -> Bool {
        return self.hashValue == rhs.hashValue
    }
    
    static func == (lhs: ScriptOperator, rhs: ScriptOperator) -> Bool {
        if lhs.hashValue == rhs.hashValue {
            if let lhsArgs = lhs.args, let rhsArgs = rhs.args {
                if lhsArgs.count != rhsArgs.count {
                    return false
                }
                for (key, lhsValue) in lhsArgs {
                    if let rhsValue = rhsArgs[key] {
                        return equalsOptionalAny(lhs: lhsValue, rhs: rhsValue)
                    }
                    return false
                }
            } else if (lhs.args != nil && rhs.args == nil) || (lhs.args == nil && rhs.args != nil) {
                return false
            }
            if let lhsDirectiveArgs = lhs.directiveArgs, let rhsDirectiveArgs = rhs.directiveArgs {
                if lhsDirectiveArgs.count != rhsDirectiveArgs.count {
                    return false
                }
                for (key, lhsValue) in lhsDirectiveArgs {
                    var flag = false
                    if let rhsValue = rhsDirectiveArgs[key] {
                        flag = lhsValue == rhsValue
                    }
                    if !flag {
                        return false
                    }
                }
            } else if (lhs.directiveArgs != nil && rhs.directiveArgs == nil) || (lhs.directiveArgs == nil && rhs.directiveArgs != nil) {
                return false
            }
            return true
        }
        return false
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(self.forCreateInstance)
        if let targetClass = self.targetClass {
            hasher.combine(targetClass)
        }
        if let group = self.group {
            hasher.combine(group)
        } else if let selector = self.selector {
            hasher.combine(selector)
        }
    }
}

extension ScriptOperator: CustomStringConvertible {
    var description: String {
        if self.forCreateInstance {
            return "create instance: \(targetClass!), selector: \(selector!)"
        } else if self.forSetProperties {
            return "setProps, instance: \(targetClass!)"
        } else if self.forCallAPIs {
            return "callAPIs, selector: \(selector!)"
        } else {
            return "onlyDirectives"
        }
    }
}

class ScriptInstruction : Copyable, Codable {
    // for invoke instructions in subscribe's Bundle
    fileprivate struct SubscribeInfo {
        let mainInstr: ScriptInstruction
        var remainInstrs = [ScriptInstruction]()
    }
    
    let category: ScriptInstructionCategory
    // origin order of script
    let index: Int
    private (set) var remark: String? = nil
    fileprivate (set) var stage: ScriptInstructionStage? = nil
    fileprivate (set) var isDefault: Bool = false
    fileprivate (set) var isReplaced: Bool = false
    fileprivate (set) var belongSettings: Bool = false
    // filled instructions and reorder
    fileprivate (set) var pos: Int = 0
    fileprivate (set) var operators = [ScriptOperator]()
    fileprivate var directives: [ScriptDirective]! = nil
    // actionName = bundleName@actionName
    fileprivate (set) var replacedByActionName = ""
    fileprivate (set) var associatedActions = Set<String>()
    fileprivate (set) var associatedActionArguments = [String : [String : ScriptDirective]]()
    fileprivate (set) var checkedPredicateContext = [ScriptOperator : Bool]()
    private var cachedSelectorInOperators: Set<String>! = nil
    
    var isStageCreateInstance: Bool {
        return self.stage != nil && self.stage! == .createInstance
    }
    var isStageConfigSettings: Bool {
        return self.stage != nil && self.stage! == .configSettings
    }
    var isStageStartRunning: Bool {
        return self.stage != nil && self.stage! == .startRunning
    }
    var isStageExitRunning: Bool {
        return self.stage != nil && self.stage! == .exitRunning
    }
    var isStageDestoryInstance: Bool {
        return self.stage != nil && self.stage! == .destoryInstance
    }
    
    func containsSelector(_ selector: String) -> Bool {
        if self.cachedSelectorInOperators == nil {
            self.cachedSelectorInOperators = []
            for op in self.operators {
                if let name = op.selector {
                    self.cachedSelectorInOperators.insert(name)
                }
            }
            let callbacks = [self.waitCallback, self.subscribeCallback]
            for case let item? in callbacks {
                if !self.cachedSelectorInOperators.contains(item.0) {
                    self.cachedSelectorInOperators.insert(item.0)
                }
                if let blockor = item.1, !self.cachedSelectorInOperators.contains(blockor) {
                    self.cachedSelectorInOperators.insert(blockor)
                }
            }
        }
        return self.cachedSelectorInOperators.contains(selector)
    }
    
    // get directives values
    private func containsDirective(_ directive: ScriptDirective) -> Bool {
        if let directives = self.directives {
            for item in directives {
                if directive.isKind(of: item) {
                    return true
                }
            }
        }
        return false
    }
    
    var isNormalInstruction: Bool {
        return !(self.delayTime > 0 || self.runInMainThread != nil || self.waitCallback != nil)
    }
    
    var isBreakPoint: Bool {
        return self.containsDirective(.breakPoint)
    }
    
    var iterateTimes: Int {
        if let directives = self.directives {
            for directive in directives {
                if case .iterate(let times) = directive {
                    return times
                }
            }
        }
        return 1
    }
    
    var delayTime: TimeInterval {
        if let directives = self.directives {
            for directive in directives {
                if case .delay(let interval) = directive {
                    return interval
                }
            }
        }
        return 0
    }
    
    var runInMainThread: Bool? {
        if let directives = self.directives {
            for directive in directives {
                if case .schedule(let runInMainThread) = directive {
                    return runInMainThread
                }
            }
        }
        return nil
    }

    var waitCallback: (String, String?, OrderedDictionary<String, String>)? {
        if let directives = self.directives {
            for directive in directives {
                if case .waitCallback(let selector, let blockor, let mapping) = directive {
                    return (selector, blockor, mapping)
                }
            }
        }
        return nil
    }
    
    var subscribeCallback: (String, String?, OrderedDictionary<String, String>)? {
        if let directives = self.directives {
            for directive in directives {
                if case .subscribeCallback(let selector, let blockor, let mapping) = directive {
                    return (selector, blockor, mapping)
                }
            }
        }
        return nil
    }
    
    var canInvoking: Bool {
        for op in self.operators {
            if let predicate = self.checkedPredicateContext[op], predicate {
                return true
            }
        }
        return false
    }
    
    // methods
    required init(instance: ScriptInstruction) {
        self.category = instance.category
        self.index = instance.index
        self.remark = instance.remark
        self.pos = instance.pos
        self.stage = instance.stage
        self.isDefault = instance.isDefault
        self.isReplaced = instance.isReplaced
        self.belongSettings = instance.belongSettings
        self.directives = instance.directives
        var operators = [ScriptOperator]()
        for op in instance.operators {
            operators.append(op.copy())
        }
        self.operators = operators
        self.associatedActions = instance.associatedActions
        self.associatedActionArguments = instance.associatedActionArguments
        self.replacedByActionName = instance.replacedByActionName
    }
    
    init(category: ScriptInstructionCategory, index: Int) {
        self.category = category
        self.index = index
    }
    
    func addDirective(_ directive: ScriptDirective) {
        if self.directives == nil {
            self.directives = []
        }
        self.directives.append(directive)
    }
    
    func addOperator(_ op: ScriptOperator) {
        self.operators.append(op)
        if self.operators.count == 1 && self.remark == nil {
            self.remark = "\(op)"
        }
    }
    
    func addAction(_ action: String, _ arguments: [String : ScriptDirective]?) {
        // prefix of build-in action name is "__"
        var actionName = action
        if !action.hasPrefix("__") {
            actionName = "custom.\(action)"
            if let arguments = arguments {
                self.associatedActionArguments[actionName] = arguments
            }
        }
        self.associatedActions.insert(actionName)
    }
    
    func setRemark(_ remark: String) {
        self.remark = remark
    }
    
    func setStage(_ stage: ScriptInstructionStage) {
        self.stage = stage
    }
    
    func setReplacedActionName(_ actionName: String) {
        if actionName.count > 0 {
            self.replacedByActionName = actionName
        }
    }
    
    fileprivate func containsOperator(_ op: ScriptOperator) -> Bool {
        for item in self.operators {
            if item.isKind(of: op) {
                return true
            }
        }
        return false
    }
    
    fileprivate func canInvoke(_ invocation: InstructionInvocation, _ mode: ScriptsInvokeMode, _ instrContext: [String : Any]) -> Bool {
        self.checkedPredicateContext.removeAll()
        var flag = 0
        for op in operators {
            let check = invocation.predicateOperator(op, mode, instrContext)
            self.checkedPredicateContext[op] = check
            flag = flag | (check ? 1 : 0)
        }
        return flag == 1
    }

    fileprivate func invoke(with invocation: InstructionInvocation, mode: ScriptsInvokeMode, instrContext: [String : Any]=[:]) {
        var currInvokeValue: Any? = nil
        
        for op in operators {            
            if self.checkedPredicateContext[op]! {
                if let result = invocation.invoke(mode: mode, op,
                                                  preInvokeValue: currInvokeValue,
                                                  instrStage: self.stage,
                                                  instrContext: instrContext) {
                    currInvokeValue = result
                }
            }
        }
    }
    
    fileprivate func updateDirectives(with other: ScriptInstruction) {
        if self.stage == nil && other.stage != nil {
            self.stage = other.stage
        }
        for action in other.associatedActions {
            if !self.associatedActions.contains(action) {
                self.associatedActions.insert(action)
            }
        }
        if let otherDirectives = other.directives {
            for directive in otherDirectives {
                if !self.containsDirective(directive) {
                    self.addDirective(directive)
                }
            }
        }
        for otherOp in other.operators {
            for op in self.operators {
                if op.isKind(of: otherOp) {
                    op.updateDirectives(with: otherOp)
                }
            }
        }
    }
    
    fileprivate func updateDirectives(onlyOneOperator: ScriptOperator) {
        for op in self.operators {
            if op.isKind(of: onlyOneOperator) {
                op.updateDirectives(with: onlyOneOperator)
            }
        }
    }
}

extension ScriptInstruction : Equatable {
    static func == (lhs: ScriptInstruction, rhs: ScriptInstruction) -> Bool {
        if lhs.operators.count == rhs.operators.count {
            for op in rhs.operators {
                if !lhs.containsOperator(op) {
                    return false
                }
            }
            return true
        }
        return false
    }
}

// REMARK: InstructionInvocation & SDK Implement

protocol InstructionInvocation {
    var category: ScriptInstructionCategory { get }
    
    func getWaitRACSubject(selector: String, blockor: String?) -> IOSubject<Void>?
    func addCallbackObserver(selector: String, forBlock: Bool,
                             _ callbackFn: @escaping Action2<InstructionInvocation, [String : Any]>) -> UUID
    func removeCallbackObserver(callbackId: UUID)
    
    func fillRequiredInstructions(_ instructions: [ScriptInstruction]) -> [ScriptInstruction]
    func predicateOperator(_ op: ScriptOperator, _ mode: ScriptsInvokeMode, _ instrContext: [String : Any]) -> Bool
    func invoke(mode: ScriptsInvokeMode, _ op: ScriptOperator, preInvokeValue: Any?, instrStage: ScriptInstructionStage?, instrContext: [String : Any]) -> Any?
    func stop(_ stopRunning: Bool, _ forDestory: Bool)
    func cleanContext()
}

class InstructionSDKInvocation : InstructionInvocation, ScriptEngineDispatch {
    deinit {
        self.engine.removeTarget(target: self)
        print("TODO: remove, test deinit when call LoadEngine")
    }
    let mutex = Mutex()
    var category: ScriptInstructionCategory {
        return .sdk
    }
    private weak var engine: ScriptEngine!
    private let handleDirectiveArgFn: Function3<ScriptDirective, ScriptsInvokeMode, [String : Any], Any?>
    private var waitCallbackSelectors = Set<String>()
    private var waitBlockSelectors = Set<String>()
    private var blocksMappingSelectors = [String : String]()
    // for .waitCallback
    private var waitCallbackSubjects = [String : IOSubject<Void>]()
    // for .subscribeCallback
    private var subscribeCallbacks = [String : [UUID : Action2<InstructionInvocation, [String : Any]>]]()
    private var cachedBlockArgsMapping = [String : [String : String]]()
    private var stageCreateInstance: ScriptInstruction! = nil
    private var stageConfigSettings: ScriptInstruction! = nil
    private var stageStartRunning: ScriptInstruction! = nil
    private var stageStopRunning: ScriptInstruction! = nil
    private var stageDestoryInstance: ScriptInstruction! = nil
    private (set) var currStopRunning: ScriptInstruction! = nil
    private (set) var currDestoryInstance: ScriptInstruction! = nil
    
    init(engine: ScriptEngine, handleDirectiveArgFn: @escaping Function3<ScriptDirective, ScriptsInvokeMode, [String : Any], Any?>) {
        self.handleDirectiveArgFn = handleDirectiveArgFn
        self.engine = engine
        self.engine.addTarget(target: self, runInMainQueue: true)
        // set default instructions
        
        // create instance
        let opCreate = ScriptOperator(selector: "sharedEngineWithAppId:delegate:", group: "sharedEngine")
        opCreate.addDirectiveArg("appId", .contextStates(keyPath: "main.appId", initValue: nil, control: nil))
        opCreate.addArgValue("delegate", ())
        opCreate.addCallActionDirectives(.waitCallAPI(mapping: ["actionEngineRunning": "return"]), nil)
        self.stageCreateInstance = buildDefaultSDKInstruction()
        self.stageCreateInstance.addOperator(opCreate)
        self.stageCreateInstance.addDirective(.breakPoint)
        self.stageCreateInstance.addAction("__createInstance", nil)
        self.stageCreateInstance.stage = .createInstance
        // config settings
        let opSetChannelProfile = ScriptOperator(selector: "setChannelProfile:", group: nil)
        opSetChannelProfile.addDirectiveArg("profile", .contextStates(keyPath: "main.channelProfile",
                                                                      initValue: nil, control: nil))
        let opSetClientRole = ScriptOperator(selector: "setClientRole:", group: nil)
        opSetClientRole.addDirectiveArg("role", .contextStates(keyPath: "main.clientRole",
                                                               initValue: nil, control: nil))
        self.stageConfigSettings = buildDefaultSDKInstruction()
        self.stageConfigSettings.addOperator(opSetChannelProfile)
        self.stageConfigSettings.addOperator(opSetClientRole)
        self.stageConfigSettings.stage = .configSettings
        // join channel
        let opJoinChannel = ScriptOperator(selector: "joinChannelByToken:channelId:info:uid:joinSuccess:",
                                              group: "joinChannel")
        opJoinChannel.addDirectiveArg("channelId", .contextStates(keyPath: "main.channelId", initValue: nil, control: nil))
        opJoinChannel.addDirectiveArg("uid", .contextStates(keyPath: "main.localUID", initValue: 0, control: nil))
        opJoinChannel.addDirectiveArg("token", .contextArgToken)
        opJoinChannel.addCallActionDirectives(.waitCallAPI(mapping: ["willJoinChannel": getCustomStateValue(true)]), nil)
        self.stageStartRunning = buildDefaultSDKInstruction()
        self.stageStartRunning.addOperator(opJoinChannel)
        self.stageStartRunning.addDirective(.breakPoint)
        // add custom value `true` to 'hasJoined'
        var customStateValue = getCustomStateValue(true)
        self.stageStartRunning.addDirective(.waitCallback(selector: "rtcEngine:didJoinChannel:withUid:elapsed:",
                                                          blockor: "joinSuccessBlockWithSender:NSString:NSUInteger:NSInteger:",
                                                          mapping: [
                                                            "main.localUID": "uid",
                                                            "hasJoined": customStateValue
                                                          ]))
        self.stageStartRunning.addAction("__joinChannel", nil)
        self.stageStartRunning.stage = .startRunning
        // leave channel
        let opLeaveChannel = ScriptOperator(selector: "leaveChannel:", group: "leaveChannel")
        opLeaveChannel.addCallActionDirectives(.waitCallAPI(mapping: ["willJoinChannel": getCustomStateValue(false)]), nil)
        self.stageStopRunning = buildDefaultSDKInstruction()
        self.stageStopRunning.addOperator(opLeaveChannel)
        self.stageStopRunning.addDirective(.breakPoint)
        // add custom value `false` to 'hasJoined'
        customStateValue = getCustomStateValue(false)
        self.stageStopRunning.addDirective(.waitCallback(selector: "rtcEngine:didLeaveChannelWithStats:",
                                                         blockor: "leaveChannelBlockWithSender:AgoraChannelStats:",
                                                         mapping: ["hasJoined": customStateValue]))
        self.stageStopRunning.addAction("__leaveChannel", nil)
        self.stageStopRunning.stage = .exitRunning
        // destory instance
        let opDestroy = ScriptOperator(selector: "destroy", group: "destroy")
        opDestroy.addCallActionDirectives(.waitCallAPI(mapping: ["actionEngineStop": ""]), nil)
        self.stageDestoryInstance = buildDefaultSDKInstruction()
        self.stageDestoryInstance.addOperator(opDestroy)
        self.stageDestoryInstance.addAction("__destoryInstance", nil)
        self.stageDestoryInstance.stage = .destoryInstance
    }
    
    private func buildDefaultSDKInstruction() -> ScriptInstruction {
        let instruction = ScriptInstruction(category: .sdk, index: -1)
        instruction.isDefault = true
        return instruction
    }
    
    // mapping block arg to delegate arg for .waitCallback
    func getBlockMappingArgs(_ selector: String, _ blockor: String) -> [String : String] {
        if let result = self.cachedBlockArgsMapping[blockor] {
            return result
        }
        var result = [String : String]()
        var blockSignatureNode: AstNode? = nil
        goto: for node in self.engine.getAllClassNodes() {
            for method in node.children where method.type == .method {
                for param in method.children {
                    if let blockSignature = param.value?.blockSignature, blockSignature.name == blockor {
                        blockSignatureNode = blockSignature
                        break goto
                    }
                }
            }
        }
        if let delegateNode = self.engine.findDelegateMethodNode(by: selector), let blockNode = blockSignatureNode,
           delegateNode.children.count == delegateNode.children.count {
            for i in (0..<(delegateNode.children.count-1)).reversed() {
                let blockArgNode = blockNode.children[i]
                let delegateArgNode = delegateNode.children[i+1]
                if blockNode.declType! == delegateNode.declType! {
                    result[delegateArgNode.name] = blockArgNode.name
                }
            }
        }
        self.cachedBlockArgsMapping[blockor] = result
        return result
    }

    // for .waitCallback
    func getWaitRACSubject(selector: String, blockor: String?) -> IOSubject<Void>? {
        var result: IOSubject<Void>? = nil
        let succ = mutex.tryLock()
        if let subject = self.waitCallbackSubjects[selector] {
            result = subject
        } else {
            if let mappingSelector = blockor {
                self.waitBlockSelectors.insert(mappingSelector)
                self.blocksMappingSelectors[mappingSelector] = selector
            }
            self.waitCallbackSelectors.insert(selector)
            let subject = IOSubject<Void>()
            self.waitCallbackSubjects[selector] = subject
            result = subject
        }
        if succ {
            mutex.unlock()
        }
        return result
    }
    
    // for .subscribe
    func addCallbackObserver(selector: String, forBlock: Bool,
                             _ callbackFn: @escaping Action2<InstructionInvocation, [String : Any]>) -> UUID {
        let succ = mutex.tryLock()
        let callbackId = UUID()
        var callbacks = self.subscribeCallbacks[selector] ?? [UUID : Action2<InstructionInvocation, [String : Any]>]()
        callbacks[callbackId] = callbackFn
        self.subscribeCallbacks[selector] = callbacks
        if succ {
            mutex.unlock()
        }
        return callbackId
    }
    
    // remove .subscribe callbackFn
    func removeCallbackObserver(callbackId: UUID) {
        var callbacks: [UUID : Action2<InstructionInvocation, [String : Any]>]? = nil
        for (_, item) in self.subscribeCallbacks {
            if item[callbackId] != nil {
                callbacks = item
                break
            }
        }
        callbacks?.removeAll()
    }
    
    func fillRequiredInstructions(_ instructions: [ScriptInstruction]) -> [ScriptInstruction] {
        var filledInstructions = [ScriptInstruction]()
        // find required instructions
        var indexCreateInstance = -1
        var indexConfigChannelProfile = -1
        var indexConfigClientRole = -1
        var indexStartRunning = -1
        var indexStopRunning = -1
        var indexDestoryInstance = -1
        for i in 0..<instructions.count {
            let instruction = instructions[i]
            if instruction.isStageCreateInstance || instruction.containsOperator(self.stageCreateInstance.operators.first!) {
                indexCreateInstance = i
            } else if instruction.isStageStartRunning || instruction.containsOperator(self.stageStartRunning.operators.first!) {
                indexStartRunning = i
            } else if instruction.isStageExitRunning || instruction.containsOperator(self.stageStopRunning.operators.first!) {
                indexStopRunning = i
            } else if instruction.isStageDestoryInstance || instruction.containsOperator(self.stageDestoryInstance.operators.first!) {
                indexDestoryInstance = i
            } else {
                if instruction.containsOperator(self.stageConfigSettings.operators[0]) {
                    indexConfigChannelProfile = i
                }
                if instruction.containsOperator(self.stageConfigSettings.operators[1]) {
                    indexConfigClientRole = i
                }
            }
            filledInstructions.append(instruction)
        }
        // check order of required instructions
        if !(indexCreateInstance == -1 || indexCreateInstance == 0) {
            fatalError("Load Script's Bundles Error: instruction of CreateSDKInstance must be first on Main Bundles")
        }
        if indexStartRunning == -1 {
            fatalError("Load Script's Bundles Error: instruction of JoinChannel is required on Main Bundles")
        }
        // insert default instructions or update directives
        instructions[indexStartRunning].updateDirectives(with: self.stageStartRunning)
        if indexCreateInstance == -1 {
            filledInstructions.insert(self.stageCreateInstance, at: 0)
        } else {
            instructions[indexCreateInstance].updateDirectives(with: self.stageCreateInstance)
        }
        if indexConfigChannelProfile == -1 && indexConfigClientRole == -1 {
            filledInstructions.insert(self.stageConfigSettings, at: indexStartRunning)
        } else {
            var lastInstr: ScriptInstruction! = nil
            if indexConfigChannelProfile != -1 {
                let instr = instructions[indexConfigChannelProfile]
                instr.updateDirectives(onlyOneOperator: self.stageConfigSettings.operators[0])
                lastInstr = instr
            }
            if indexConfigClientRole != -1 {
                let instr = instructions[indexConfigClientRole]
                instr.updateDirectives(onlyOneOperator: self.stageConfigSettings.operators[1])
                lastInstr = instr
            }
            if indexConfigChannelProfile == -1 {
                lastInstr.addOperator(self.stageConfigSettings.operators[0])
            }
            if indexConfigClientRole == -1 {
                lastInstr.addOperator(self.stageConfigSettings.operators[1])
            }
            lastInstr.stage = .configSettings
        }
        if indexStopRunning == -1 {
            if indexDestoryInstance != -1 {
                filledInstructions.insert(self.stageStopRunning, at: indexDestoryInstance)
            }
            self.currStopRunning = self.stageStopRunning
        } else {
            self.currStopRunning = instructions[indexStopRunning]
            self.currStopRunning.updateDirectives(with: self.stageStopRunning)
        }
        if indexDestoryInstance == -1 {
            self.currDestoryInstance = self.stageDestoryInstance
        } else {
            self.currDestoryInstance = instructions[indexDestoryInstance]
            self.currDestoryInstance.updateDirectives(with: self.stageDestoryInstance)
        }
        return filledInstructions
    }
    
    func stop(_ stopRunning: Bool, _ forDestory: Bool) {
        // detect context.hasJoined
        if stopRunning {
            if let result = handleDirectiveArgFn(.predicateContext(keyPath: "hasJoined", compareOp: "=",
                                                                   value: true, control: nil), .automatic, [:]) as? Bool, result {
                if self.currStopRunning.canInvoke(self, .semiAutomatic, [:]) {
                    self.currStopRunning.invoke(with: self, mode: .semiAutomatic)
                }
            }
        }
        // detect engine.isRunning
        if forDestory {
            if let result = handleDirectiveArgFn(.predicateContext(keyPath: "isEngineRunning", compareOp: "=",
                                                                   value: true, control: nil), .automatic, [:]) as? Bool, result {
                if self.currDestoryInstance.canInvoke(self, .semiAutomatic, [:]) {
                    self.currDestoryInstance.invoke(with: self, mode: .semiAutomatic)
                }
            }
            self.currStopRunning = nil
            self.currDestoryInstance = nil
        }
    }
    
    func cleanContext() {
        let succ = mutex.tryLock()
        self.waitCallbackSelectors.removeAll()
        self.waitBlockSelectors.removeAll()
        self.blocksMappingSelectors.removeAll()
        self.waitCallbackSubjects.removeAll()
        self.subscribeCallbacks.removeAll()
        if succ {
            mutex.unlock()
        }
    }
    
    func predicateOperator(_ op: ScriptOperator, _ mode: ScriptsInvokeMode, _ instrContext: [String : Any]) -> Bool {
        let predicateList: [ScriptDirective?] = [op.predicateContextDirective, op.predicateRespondsDirective]
        for case let predicate? in predicateList {
            if let result = handleDirectiveArgFn(predicate, mode, instrContext) as? Bool, result {
                continue
            } else {
                return false
            }
        }
        return true
    }
    
    func invoke(mode: ScriptsInvokeMode,
                _ op: ScriptOperator,
                preInvokeValue: Any?,
                instrStage: ScriptInstructionStage?,
                instrContext: [String : Any]) -> Any? {
        var preInstance = preInvokeValue
        var argValues: [Any?]? = nil
        var targetNode: AstNode! = nil
        if op.forInvokeDirectives {
            // sync init states to appContext
            var actionArgs = [String : String]()
            if let directiveArgs = op.directiveArgs {
                for (keyPath, arg) in directiveArgs {
                    if case .contextStates = arg {
                        let val = self.handleDirectiveArgFn(arg, mode, instrContext)
                        let (_, valStr) = getAnyValueStr(from: val)
                        actionArgs[keyPath] = valStr
                    }
                }
            }
            if let args = op.args {
                for (keyPath, arg) in args {
                    let (_, valStr) = getAnyValueStr(from: arg)
                    actionArgs[keyPath] = valStr
                }
            }
            // call actions on appContext
            if let contextCallAction = op.contextCallActionDirective {
                var directive = contextCallAction
                if actionArgs.count >= 1, case .contextCallAction(let name, _) = contextCallAction {
                    directive = ScriptDirective.contextCallAction(methodName: name, args: actionArgs)
                }
                let _ = self.handleDirectiveArgFn(directive, mode, instrContext)
            }
            return nil
        }
        
        if let targetClass = op.targetClass, let node = engine.findAstNode(by: targetClass),
            node.type == .interface {
            targetNode = node
        }
        if let selector = op.selector {
            var methodNode: AstNode! = nil
            if targetNode != nil {
                for node in targetNode.children where node.type == .method {
                    if node.name == selector {
                        methodNode = node
                        break
                    }
                }
                if op.forCreateInstance {
                    if selector == "init" {
                        preInstance = engine.createInstance(op.targetClass, false, nil, nil)
                    } else {
                        argValues = self.getArgValues(mode: mode,
                                                      method: methodNode,
                                                      args: op.args,
                                                      directiveArgs: op.directiveArgs,
                                                      preInstance: preInstance,
                                                      instrContext: instrContext)
                        preInstance = engine.createInstance(op.targetClass,
                                                            methodNode.value!.isStaticValue,
                                                            selector,
                                                            argValues)
                        op.cleanArgs()
                    }
                } else {
                    engine.invoke(instance: targetNode,
                                  op.targetClass,
                                  methodNode.value!.isStaticValue,
                                  selector,
                                  argValues ?? [])
                }
            } else {
                var realSelector = selector
                var actionNameForSameSelector = ""
                let split = selector.components(separatedBy: "@")
                if split.count == 2 {
                    realSelector = split[1]
                    actionNameForSameSelector = split[0]
                }
                methodNode = engine.findAPIMethodNode(by: realSelector)
                argValues = self.getArgValues(mode: mode,
                                              method: methodNode,
                                              args: op.args,
                                              directiveArgs: op.directiveArgs,
                                              preInstance: preInstance,
                                              instrContext: instrContext)
                if op.forCreateInstance {
                    preInstance = engine.createInstance(nil, methodNode.value!.isStaticValue, methodNode.name, argValues)
                } else {
                    if engine.isRunning || (instrStage != nil && instrStage! == .createInstance) {
                        // handle __ctx args
                        var callAPICtx = [String : Any]()
                        if actionNameForSameSelector.count > 0 {
                            callAPICtx["actionNameForSameSelector"] = actionNameForSameSelector
                        } else {
                            for (key, value) in instrContext {
                                if key.starts(with: "cellStreamId") {
                                    callAPICtx[key] = value
                                }
                            }
                        }
                        engine.invoke(realSelector, callAPICtx: callAPICtx, parameters: argValues ?? [])
                    } else {
                        fatalError("\(engine!) not start running.")
                    }
                }
            }
        }
        if op.forSetProperties {
            // REMARK: set instance = preInvokeValue, to call set property value
            let instance = preInstance! as AnyObject
            var properties = Set([String]())
            if let args = op.directiveArgs {
                for (key, arg) in args {
                    let value = handleDirectiveArgFn(arg, mode, instrContext)
                    engine.setValue(value, key, op.targetClass, instance)
                    properties.insert(key)
                }
            }
            if let args = op.args {
                for (key, arg) in args {
                    if !properties.contains(key) {
                        engine.setValue(arg, key, op.targetClass, instance)
                    }
                }
            }
        }
        return preInstance
    }
    
    private func getArgValues(mode: ScriptsInvokeMode,
                              method: AstNode,
                              args: [String : Any?]?,
                              directiveArgs: [String : ScriptDirective]?,
                              preInstance: Any?,
                              instrContext: [String : Any]) -> [Any?]? {
        var argValues: [Any?]! = nil
        if method.children.count > 0 {
            argValues = []
        }
        for param in method.children {
            if let directive = directiveArgs?[param.name] {
                if case .preOperatorInstance = directive {
                    if let instance = preInstance {
                        argValues.append(instance as AnyObject)
                    }
                } else if let value = instrContext[param.name] {
                    argValues.append(value)
                } else {
                    argValues.append(handleDirectiveArgFn(directive, mode, instrContext))
                }
            } else if let value = args?[param.name] {
                argValues.append(value)
            } else {
                argValues.append(nil)
            }
        }
        return argValues
    }
    
    // ScriptEngine Dispatch
    func responseDelegate(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], isSDKInstance: Bool, instanceClass: String) {
        let succ = mutex.tryLock()
        if self.waitCallbackSelectors.contains(selector),
           let subject = self.waitCallbackSubjects[selector] {
            subject.sendNext(())
            subject.sendComplete()
            self.waitCallbackSubjects[selector] = IOSubject<Void>()
        }
        if let callbacks = self.subscribeCallbacks[selector] {
            for (_, callback) in callbacks {
                callback(self, argValues)
            }
        }
        if succ {
            mutex.unlock()
        }
    }
    
    func responseBlock(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                       isSDKInstance: Bool, instanceClass: String) {
        let succ = mutex.tryLock()
        if self.waitBlockSelectors.contains(selector),
            let delegateSelector = self.blocksMappingSelectors[selector],
            let subject = self.waitCallbackSubjects[delegateSelector] {
            subject.sendNext(())
            subject.sendComplete()
            self.waitCallbackSubjects[delegateSelector] = IOSubject<Void>()
        }
        if let delegateSelector = self.blocksMappingSelectors[selector],
            let callbacks = (self.subscribeCallbacks[selector] ?? self.subscribeCallbacks[delegateSelector]) {
            var args = [String : Any]()
            let argsMapping = self.self.getBlockMappingArgs(delegateSelector, selector)
            for (delegateArg, blockArg) in argsMapping {
                args[delegateArg] = argValues[blockArg]
            }
            for (_, callback) in callbacks {
                callback(self, args)
            }
        }
        if succ {
            mutex.unlock()
        }
    }
    
    func responseAPI(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                     returnValue: Any, isSDKInstance: Bool, instanceClass: String) { }
}
// REMARK: Bundle & Invoker

enum ScriptBundleType : String, Codable {
    case `default` = "default"
    case custom = "custom"
    case scenario = "scenario"
}

class ScriptBundles : Codable {
    let type: ScriptBundleType
    private (set) var name: String! = nil
    private (set) var remark: String! = nil
    fileprivate var items = [String : [ScriptInstruction]]()
    fileprivate var insertBundle: [ScriptInstruction]! {
        return self.items["__insert"]
    }
    fileprivate var mainBundle: [ScriptInstruction]! {
        return self.items["main"]
    }
    var actionsBundle: [ScriptInstruction]! {
        return self.items["actions"]
    }
    var settingsBundle: [ScriptInstruction]! {
        return self.items["settings"]
    }
    fileprivate var subscribeBundles: [String : [ScriptInstruction]] {
        var result = [String : [ScriptInstruction]]()
        for (key, bundles) in self.items {
            if !(key == "main" || key == "actions" || key == "settings" || key == "__insert") {
                result[key] = bundles
            }
        }
        return result
    }
  
    static func loadFromCache(name: String, type: ScriptBundleType) -> ScriptBundles? {
        let jsonPath = getAppRootPath()
        let typeName = type == .default ? type.rawValue : "\(type)_"
        let fileName = "script_\(typeName)\(name).json"
        let filePath = jsonPath + "/Scripts/\(fileName)"
        if FileManager.default.fileExists(atPath: filePath) {
            if let configStr = try? String(contentsOfFile: filePath),
                let data = configStr.data(using: .utf8) {
                return try! JSONDecoder().decode(ScriptBundles.self, from: data)
            }
        }
        return nil
    }
    
    init(type: ScriptBundleType, name: String, remark: String) {
        self.type = type
        if self.type != .default {
            assert(!name.isEmpty, "please input script's name")
            self.name = name
        }
        self.remark = remark
    }
    
    public func checkReplacedActionName(_ actionName: String) -> Bool {
        for (_, bundle) in self.items {
            for instr in bundle {
                if instr.associatedActions.contains(actionName) {
                    return true
                }
            }
        }
        return false
    }
    
    fileprivate func findCallAPIMappingStates(_ selector: String,
                                              _ invocation: InstructionSDKInvocation) -> OrderedDictionary<String, String>? {
        let handler = { (instruction: ScriptInstruction) -> OrderedDictionary<String, String>? in
            var delegateSelector = selector
            var blockSelector: String! = nil
            let mapping: OrderedDictionary<String, String>! = [:]
            // find mapping info
            for op in instruction.operators {
                if let opName = op.selector, opName == selector, let directive = op.waitCallAPIDirective {
                    if case .waitCallAPI(let values) = directive {
                        mapping.mergeValues(by: values)
                    }
                }
            }
            if let waitCallback = instruction.waitCallback {
                if waitCallback.0 == selector || (waitCallback.1 != nil && waitCallback.1! == selector) {
                    mapping.mergeValues(by: waitCallback.2)
                    if waitCallback.1 != nil {
                        delegateSelector = waitCallback.0
                        blockSelector = waitCallback.1
                    }
                }
            } else if let subscribeCallback = instruction.subscribeCallback {
                if subscribeCallback.0 == selector || (subscribeCallback.1 != nil && subscribeCallback.1! == selector) {
                    mapping.mergeValues(by: subscribeCallback.2)
                    if subscribeCallback.1 != nil {
                        delegateSelector = subscribeCallback.0
                        blockSelector = subscribeCallback.1
                    }
                }
            }
            // handle block mapping args
            if mapping.count > 0 {
                var isBlockArg = false
                for (_, value) in mapping {
                    if value.starts(with: "#") {
                        isBlockArg = true
                        break
                    }
                }
                if (selector == blockSelector && !isBlockArg) || (selector != blockSelector && isBlockArg) {
                    let mappingArgs = invocation.getBlockMappingArgs(delegateSelector, blockSelector)
                    if selector == blockSelector {
                        for (key, value) in mapping {
                            if !value.starts(with: "#"), let updateVlue = mappingArgs[value] {
                                mapping[key] = updateVlue
                            }
                        }
                    } else if selector != blockSelector {
                        for (key, value) in mapping {
                            if value.starts(with: "#") {
                                for (delegateArg, blockArg) in mappingArgs {
                                    if value == blockArg {
                                        mapping[key] = delegateArg
                                        break
                                    }
                                }
                            }
                        }
                    }
                }
                return mapping
            }
            return nil
        }
        
        let result: OrderedDictionary<String, String> = [:]
        if let instruction = invocation.currStopRunning, instruction.containsSelector(selector) {
            if let mapping = handler(instruction) {
                result.mergeValues(by: mapping)
            }
        }
        if let instruction = invocation.currDestoryInstance, instruction.containsSelector(selector) {
            if let mapping = handler(instruction) {
                result.mergeValues(by: mapping)
            }
        }
        if let insertBundle = self.insertBundle {
            for instruction in insertBundle where instruction.containsSelector(selector) {
                if let mapping = handler(instruction) {
                    result.mergeValues(by: mapping)
                }
            }
        }
        if let mainBundle = self.mainBundle {
            for instruction in mainBundle where instruction.containsSelector(selector) {
                if let mapping = handler(instruction) {
                    result.mergeValues(by: mapping)
                }
            }
        }
        if let actionsBundle = self.actionsBundle {
            for instruction in actionsBundle where instruction.containsSelector(selector) {
                if let mapping = handler(instruction) {
                    result.mergeValues(by: mapping)
                }
            }
        }
        if let settingsBundle = self.settingsBundle {
            for instruction in settingsBundle where instruction.containsSelector(selector) {
                if let mapping = handler(instruction) {
                    result.mergeValues(by: mapping)
                }
            }
        }
        for (_, subBundles) in self.subscribeBundles {
            for instruction in subBundles where instruction.containsSelector(selector) {
                if let mapping = handler(instruction) {
                    result.mergeValues(by: mapping)
                }
            }
        }
        if result.count == 0 {
            return nil
        } else {
            return result
        }
    }
    
    func writeToLocalJson() {
        let jsonPath = getAppRootPath() + "/Scripts/"
        if let data = try? JSONEncoder().encode(self) {
            if !FileManager.default.fileExists(atPath: jsonPath) {
                try! FileManager.default.createDirectory(atPath: jsonPath,
                                                         withIntermediateDirectories: true,
                                                         attributes: nil)
            }
            let type = self.type == .default ? self.type.rawValue : "\(self.type.rawValue)_"
            let fileName = "script_\(type)\(self.name ?? "").json"
            let filePath = jsonPath + fileName
            let jsonStr = String(data: data, encoding: .utf8)!
            try! jsonStr.write(toFile: filePath, atomically: true, encoding: .utf8)
        }
    }

    func updateName(_ name: String?, remark: String?) {
        if let name = name {
            self.name = name
        }
        if let remark = remark {
            self.remark = remark
        }
    }
    
    func addInsertInstruction(_ instruction: ScriptInstruction) {
        var bundle = self.items["__insert"] ?? []
        bundle.append(instruction)
        self.items["__insert"] = bundle
    }
    
    func addMainInstruction(_ instruction: ScriptInstruction) {
        var bundle = self.items["main"] ?? []
        bundle.append(instruction)
        self.items["main"] = bundle
    }
    
    func addActionsInstruction(_ instruction: ScriptInstruction) {
        var bundle = self.items["actions"] ?? []
        bundle.append(instruction)
        self.items["actions"] = bundle
    }
    
    func addSettingsInstruction(_ instruction: ScriptInstruction) {
        var bundle = self.items["settings"] ?? []
        bundle.append(instruction)
        self.items["settings"] = bundle
    }

    func addSubscribeInstruction(_ instruction: ScriptInstruction, key: String) {
        var bundle = self.items[key] ?? []
        bundle.append(instruction)
        self.items[key] = bundle
    }
}

class ScriptsInvoker : PropsStatesContext {
    private let localStates = StatesStore()
    private var observeInstructionFn: Action2<ScriptInstruction, String>?
    private var registerInvocations = [ScriptInstructionCategory : InstructionInvocation]()
    private var observeSubscribesCallbacks = [String : UUID]()
    private var cachedMappingStates = [String : OrderedDictionary<String, String>?]()
    private var cachedMappingActions: OrderedDictionary<String, (ScriptInstruction, Int, String)>! = nil
    private(set) var bundles: ScriptBundles! = nil
    private var instructionList: [ScriptInstruction]! = nil
    private var hasCreatedInstance = false
    private var hasStartRunning = false
    private var hasExitRunning = false
    private var nextInstructionPos: Int = 0
    private var invokeDisposer: IODisposer? = nil
    private var callActionDisposer: IODisposer? = nil
    private var initInvokeMode: ScriptsInvokeMode = .manual
    @PropState
    var invokeMode: ScriptsInvokeMode = .semiAutomatic
    @PropState
    var runningMode: ScriptsRunningMode = .notStart

    // register method only be called once
    func registerInvocation(_ invocation: InstructionInvocation) {
        registerInvocations[invocation.category] = invocation
        self.bindStates()
    }

    func addProcessObserver(_ observeFn: @escaping Action2<ScriptInstruction, String>) {
        self.observeInstructionFn = observeFn
    }

    func processInstruction(_ instruction: ScriptInstruction, _ bundleName: String, _ mode: ScriptsInvokeMode) {
        if bundleName == "main" {
            // handle stage states
            if instruction.isStageCreateInstance {
                self.hasCreatedInstance = true
            } else if instruction.isStageStartRunning {
                self.hasStartRunning = true
                // for repeat click the `JoinChannel` and `LeaveChannel` button on manual-model
                self.hasExitRunning = false
            } else if instruction.isStageExitRunning {
                self.hasExitRunning = true
            } else if instruction.isStageDestoryInstance {
                self.hasCreatedInstance = false
            }
            // get next pos of instruction
            if mode != .manual {
                self.nextInstructionPos = instruction.pos + 1
            }
        }
        // call custom handler
        if let observeInstructionFn = self.observeInstructionFn {
            observeInstructionFn(instruction, bundleName)
        }
    }
    
    func findCallAPIMappingStates(_ selector: String) -> OrderedDictionary<String, String>? {
        if let mapping = self.cachedMappingStates[selector] {
            return mapping
        } else if let bundles = self.bundles, let invocation = self.registerInvocations[.sdk] as? InstructionSDKInvocation,
                  let mapping = bundles.findCallAPIMappingStates(selector, invocation) {
            self.cachedMappingStates[selector] = mapping
            return mapping
        } else {
            self.cachedMappingStates.updateValue(nil, forKey: selector)
            return nil
        }
    }
    
    // @return: (Instruction, IndexInBundle, BundleName)
    private func findCustomAction(_ action: String, forReplace: Bool=false) -> (ScriptInstruction, Int, String)? {
        if let bundles = self.bundles {
            if self.cachedMappingActions == nil {
                self.cachedMappingActions = [:]
                let allKeys = bundles.items.allKeys.sorted { (item1, item2) in
                    let args = (item1, item2)
                    switch (args) {
                    case ("main", _):
                        return true
                    case (_, "main"):
                        return false
                    case ("actions", _):
                        return true
                    case (_, "actions"):
                        return false
                    case ("settings", _):
                        return true
                    case (_, "settings"):
                        return false
                    default:
                        return item1 < item2
                    }
                }
                for i in 0..<allKeys.count {
                    let key = allKeys[i]
                    if forReplace && !(key == "actions" || key == "settings") {
                        continue
                    }
                    let bundle = key == "main" ? self.instructionList! : bundles.items[key]!
                    var stopPos = 0
                    for j in 0..<bundle.count {
                        let instr = bundle[j]
                        for name in instr.associatedActions {
                            self.cachedMappingActions[name] = (instr, j, key)
                        }
                        if instr.isStageExitRunning {
                            stopPos = j
                        }
                    }
                    if key == "main" {
                        let invocation = self.registerInvocations[.sdk] as! InstructionSDKInvocation
                        if let instr = invocation.currStopRunning, instr.associatedActions.count > 0 {
                            if stopPos == 0 {
                                stopPos = bundle.count + 1
                                for name in instr.associatedActions {
                                    self.cachedMappingActions[name] = (instr, stopPos, key)
                                }
                            }
                        }
                    }
                }
            }
            return self.cachedMappingActions[action]
        } else {
            fatalError("please call loadBundles() first.")
        }
    }

    // fetch custom actions after loaded bundles
    func getCustomActions() -> [(String, [String : ScriptDirective]?)] {
        if self.bundles != nil {
            var actions = Set<String>()
            var arguments = [String : [String : ScriptDirective]]()
            for instruction in self.instructionList {
                for action in instruction.associatedActions {
                    // prefix of build-in or cell action name is start with "__"
                    if !action.hasPrefix("__") {
                        actions.insert(action)
                    }
                }
                for (key, value) in instruction.associatedActionArguments {
                    arguments[key] = value
                }
            }
            if let actionsBundle = self.bundles.actionsBundle {
                for instruction in actionsBundle {
                    for action in instruction.associatedActions {
                        // prefix of build-in action name is "__"
                        if !action.hasPrefix("__") {
                            actions.insert(action)
                        }
                    }
                    for (key, value) in instruction.associatedActionArguments {
                        arguments[key] = value
                    }
                }
            }
            for (_, bundles) in self.bundles.subscribeBundles {
                for instruction in bundles {
                    for action in instruction.associatedActions {
                        // prefix of build-in action name is "__"
                        if !action.hasPrefix("__") {
                            actions.insert(action)
                        }
                    }
                    for (key, value) in instruction.associatedActionArguments {
                        arguments[key] = value
                    }
                }
            }
            var result = [(String, [String : ScriptDirective]?)]()
            for item in actions {
                let argument = arguments[item]
                result.append((item, argument))
            }
            return result
        } else {
            fatalError("please call loadBundles() first.")
        }
    }
    
    // @result: [stateKeyPath: (forPredicate, initValue, MappingControl)]
    func getAllStatesAndPredicates() -> [(String, (Bool, Any?, StateMappingControl?))] {
        let findBundleFn: ([ScriptInstruction]) -> OrderedDictionary<String, (Bool, Any?, StateMappingControl?)> = { bundles in
            let states = OrderedDictionary<String, (Bool, Any?, StateMappingControl?)>()
            for instruction in bundles {
                for op in instruction.operators {
                    if let args = op.directiveArgs {
                        for (_, directive) in args {
                            if case .contextStates(let key, let initValue, let control) = directive,
                               (initValue != nil || control != nil) {
                                if states[key] == nil {
                                    states[key] = (false, initValue, control)
                                }
                            }
                        }
                    }
                    if let predicate = op.predicateContextDirective,
                       case .predicateContext(let key, _, _, let control) = predicate, let mappingControl = control {
                        if states[key] == nil {
                            states[key] = (true, nil, mappingControl)
                        } else if let (forPredicate, initValue, ctrl) = states[key] {
                            if !forPredicate && ctrl == nil && initValue != nil {
                                states[key] = (true, initValue, mappingControl)
                            }
                        }
                    }
                }
            }
            return states
        }

        if self.bundles != nil {
            let states = OrderedDictionary<String, (Bool, Any?, StateMappingControl?)>()
            states.mergeValues(by: findBundleFn(self.instructionList))
            if let actionsBundle = self.bundles.actionsBundle {
                states.mergeValues(by: findBundleFn(actionsBundle))
            }
            for (_, bundles) in self.bundles.subscribeBundles {
                states.mergeValues(by: findBundleFn(bundles))
            }
            return states.toArray()
        } else {
            fatalError("please call loadBundles() at first.")
        }
    }
    
    func callCustomAction(_ action: String, instrContext: [String : Any]=[:], _ callbackFn: @escaping Action={}) {
        if let bundles = self.bundles, let result = self.findCustomAction(action) {
            var bundle = result.2 == "main" ? self.instructionList! : bundles.items[result.2]!
            var startPos = result.1
            var endPos = result.1 + 1
            if result.2 == "main" && self.invokeMode == .manual {
                if result.0.isStageStartRunning {
                    // `createInstance` is startPos == 0
                    startPos = 1
                } else if result.0.isStageExitRunning {
                    let invocation = self.registerInvocations[.sdk] as! InstructionSDKInvocation
                    if result.1 >= bundle.count {
                        bundle = [invocation.currStopRunning]
                        startPos = 0
                        endPos = 1
                    }
                }
            }
            let signal = _invoke(startPos, endPos, result.2, .manual, bundle, instrContext: instrContext, signal: just(()))
            self.callActionDisposer = signal.subscribeComplete {
                callbackFn()
            }
        } else {
            fatalError("please call loadBundles() at first")
        }
    }

    func loadBundles(_ bundles: ScriptBundles) {
        self.instructionList?.removeAll()
        // remove previous subscribes callbacks
        for (_, callbackId) in self.observeSubscribesCallbacks {
            for (_, invocation) in self.registerInvocations {
                invocation.removeCallbackObserver(callbackId: callbackId)
                invocation.cleanContext()
            }
        }
        self.observeSubscribesCallbacks.removeAll()
        self.cachedMappingStates.removeAll()
        self.cachedMappingActions?.removeAll()
        self.stop(forDestory: true)
        
        self.bundles = bundles
        // load subscribe bundles
        for (name, bundles) in self.bundles!.subscribeBundles {
            // check first instr must be .subscribe directive
            if let firstInstruction = bundles.first, firstInstruction.subscribeCallback != nil {
                var subInstrList = [ScriptInstruction.SubscribeInfo]()
                var currSubscribeInfo: ScriptInstruction.SubscribeInfo! = nil
                // get subscribe info
                var pos = 0
                for instr in bundles {
                    var copyedInstr: ScriptInstruction! = nil
                    if instr.replacedByActionName.count > 0,
                       let result = self.findCustomAction(instr.replacedByActionName, forReplace: true) {
                        copyedInstr = result.0.copy()
                        copyedInstr.isReplaced = true
                    } else {
                        copyedInstr = instr.copy()
                    }
                    copyedInstr.pos = pos
                    pos += 1
                    if copyedInstr.subscribeCallback != nil {
                        if currSubscribeInfo != nil {
                            subInstrList.append(currSubscribeInfo)
                        }
                        currSubscribeInfo = ScriptInstruction.SubscribeInfo(mainInstr: copyedInstr)
                    } else {
                        currSubscribeInfo.remainInstrs.append(copyedInstr)
                    }
                }
                if currSubscribeInfo != nil {
                    subInstrList.append(currSubscribeInfo)
                }
                // invoke instructions in subscribe
                for info in subInstrList {
                    let invocation = self.registerInvocations[info.mainInstr.category]!
                    if let (selector, blockor, mapping) = info.mainInstr.subscribeCallback {
                        let forBlock = blockor != nil
                        let callbackId = invocation.addCallbackObserver(selector: selector,
                                                                        forBlock: forBlock) { (context, args) in
                            // handle cell mapping streamId
                            var cellAPICtx = args
                            var cellMappingKey = ""
                            for (_, mappingKey) in mapping {
                                if let streamId = parseCellMappingStreamId(mappingKey, args) {
                                    cellMappingKey = mappingKey
                                    cellAPICtx[getCellMappingStreamId("", streamId)] = ""
                                    break
                                }
                            }
                            // remove cellStreamId^*^ArgName and save cellStreamId^*^StreamId
                            cellAPICtx.removeValue(forKey: cellMappingKey)
                            // call instrs
                            if info.mainInstr.canInvoke(context, .automatic, cellAPICtx) {
                                info.mainInstr.invoke(with: context, mode: .automatic, instrContext: cellAPICtx)
                            }
                            if let observeInstructionFn = self.observeInstructionFn {
                                observeInstructionFn(info.mainInstr, name)
                            }
                            for instr in info.remainInstrs {
                                if instr.canInvoke(context, .automatic, cellAPICtx) {
                                    instr.invoke(with: context, mode: .automatic, instrContext: cellAPICtx)
                                }
                                if let observeInstructionFn = self.observeInstructionFn {
                                    observeInstructionFn(info.mainInstr, name)
                                }
                            }
                        }
                        self.observeSubscribesCallbacks[name] = callbackId
                    }
                }
            }
        }
        // fill missing instructions and set position for them
        if var mainBundle = bundles.mainBundle {
            for (_, invocation) in self.registerInvocations {
                mainBundle = invocation.fillRequiredInstructions(mainBundle)
            }
            // fill settingsBundle's instructions
            if let settingsBundle = self.bundles.settingsBundle,
               let index = mainBundle.firstIndex(where: { $0.isStageConfigSettings }) {
                var settingsInstrutions = [ScriptInstruction]()
                for instr in settingsBundle {
                    let copyedInstr = instr.copy()
                    copyedInstr.belongSettings = true
                    settingsInstrutions.append(copyedInstr)
                }
                mainBundle.insert(contentsOf: settingsInstrutions, at: index)
            }

            var pos = 0
            var instructions = [ScriptInstruction]()
            for instr in mainBundle {
                var copyedInstr: ScriptInstruction! = nil
                if instr.replacedByActionName.count > 0,
                   let result = self.findCustomAction(instr.replacedByActionName, forReplace: true) {
                    copyedInstr.isReplaced = true
                    copyedInstr = result.0.copy()
                } else {
                    copyedInstr = instr.copy()
                }
                copyedInstr.pos = pos
                instructions.append(copyedInstr)
                pos += 1
                if instr.isDefault || instr.isReplaced || instr.belongSettings {
                    self.bundles.addInsertInstruction(instr)
                }
            }
            self.instructionList = instructions
        }
        self.cachedMappingActions = nil
    }
    
    func invoke(mode: ScriptsInvokeMode?=nil) {
        if self.runningMode == .notStart {
            if let mode = mode {
                self.initInvokeMode = mode
                self.$invokeMode.update(value: mode, self.localStates)
            }

            self.$runningMode.update(value: .running, self.localStates)
            let signal = _invoke(0, self.instructionList.count, "main", self.invokeMode, self.instructionList,
                                 instrContext: [:], signal: just(()))
            self.invokeDisposer = signal.subscribeComplete {[weak self] in
                if let self = self {
                    self.pauseOrStop()
                }
            }
        } else {
            self.stop(forDestory: false)
        }
    }
    
    func stepNext() {
        if self.invokeMode == .semiAutomatic && self.runningMode == .pause {
            self.$runningMode.update(value: .running, self.localStates)
            let signal = _invoke(self.nextInstructionPos, self.instructionList.count, "main",
                                 self.invokeMode, self.instructionList, instrContext: [:], signal: just(()))
            self.invokeDisposer = signal.subscribeComplete {[weak self] in
                if let self = self {
                    self.pauseOrStop()
                }
            }
        }
    }
    
    func stop(forDestory: Bool) {
        if self.callActionDisposer != nil {
            self.callActionDisposer?.dispose()
            self.callActionDisposer = nil
        }
        if self.invokeDisposer != nil {
            self.invokeDisposer?.dispose()
            self.invokeDisposer = nil
        }
        let needStopRunning = self.runningMode != .notStart && self.hasStartRunning && !self.hasExitRunning
        let needDestoryInstance = forDestory && self.hasCreatedInstance
        if needDestoryInstance || needStopRunning {
            for (_ , invocation) in self.registerInvocations {
                invocation.stop(needStopRunning, needDestoryInstance)
            }
        }
        if self.runningMode != .notStart {
            self.$runningMode.update(value: .notStart, self.localStates)
        }
        if self.invokeMode != self.initInvokeMode {
            self.$invokeMode.update(value: self.initInvokeMode, self.localStates)
        }
        self.hasStartRunning = false
        self.hasExitRunning = false
        self.nextInstructionPos = 0
        if needDestoryInstance {
            self.hasCreatedInstance = false
        }
    }
    
    func pauseOrStop() {
        if self.runningMode == .running {
            if self.nextInstructionPos == self.instructionList.count {
                if !self.hasExitRunning {
                    self.$invokeMode.update(value: .manual, self.localStates)
                    self.$runningMode.update(value: .pause, self.localStates)
                } else {
                    self.stop(forDestory: false)
                }
            } else {
                self.$runningMode.update(value: .pause, self.localStates)
            }
        }
    }
    
    // assembly instruction execution sequence with IOMonad(RACSignal)
    private func _invoke(_ startPos: Int, _ totalInstructions: Int, _ bundleName: String, _ mode: ScriptsInvokeMode,
                         _ instructionList: [ScriptInstruction], instrContext: [String : Any], signal: IOMonad<Void>) -> IOMonad<Void> {
        var pos = startPos
        var mapSignal: IOMonad<Void> = signal
        if pos < totalInstructions {
            var needAsyncInvoke = false
            var loop = true
            while loop {
                let (endPos, breakState) = self.findNormalInstructionEndPos(pos, totalInstructions, mode, instructionList)
                let isBreakPoint = breakState != nil ? breakState! : false
                let syncToEnd = breakState == nil
                if !needAsyncInvoke {
                    needAsyncInvoke = syncToEnd ? false : !isBreakPoint
                }
                for i in pos...endPos {
                    let instr = instructionList[i]
                    let invocation = self.registerInvocations[instr.category]!
                    if !(self.hasCreatedInstance && instr.isStageCreateInstance) {
                        if instr.isNormalInstruction {
                            if needAsyncInvoke {
                                // for concat pre asyncInvoke signal
                                mapSignal = mapSignal.doNext {[weak self] _ in
                                    if let self = self {
                                        Self.invokeWithInstruction(instr, invocation, bundleName, mode, nil,
                                                                   self.processInstruction(_:_:_:), instrContext: instrContext)
                                    }
                                }
                            } else {
                                // just sync to invoke
                                Self.invokeWithInstruction(instr, invocation, bundleName, mode, nil,
                                                           self.processInstruction(_:_:_:), instrContext: instrContext)
                            }
                        } else if let signal = Self.invokeWithInstruction(instr, invocation, bundleName, self.invokeMode, mapSignal,
                                                                          self.processInstruction(_:_:_:), instrContext: instrContext) {
                            mapSignal = signal
                        }
                    }
                }
                if endPos + 1 < totalInstructions {
                    pos = endPos + 1
                    loop = syncToEnd ? false : !isBreakPoint
                } else {
                    loop = false
                }
            }
        }
        return mapSignal
    }
    
    // immediate execution an instruction or map/flatMap signal to async invoke
    @discardableResult
    private static func invokeWithInstruction(_ instr: ScriptInstruction,
                                              _ invocation: InstructionInvocation,
                                              _ bundleName: String,
                                              _ mode: ScriptsInvokeMode,
                                              _ preSignal: IOMonad<Void>?,
                                              _ processFn: @escaping Action3<ScriptInstruction, String, ScriptsInvokeMode>,
                                              instrContext: [String : Any]=[:]) -> IOMonad<Void>? {
        // signal != nil that means findNormalInstructionEndPos()'s result is (pos, false)
        var mapSignal: IOMonad<Void>! = preSignal
        if !instr.canInvoke(invocation, mode, instrContext) {
            return mapSignal
        }
        for _ in 0..<instr.iterateTimes {
            if instr.delayTime > 0 {
                let runInMainThread = instr.runInMainThread ?? false
                mapSignal = mapSignal.flatMap { _ in
                    return timer(instr.delayTime, delay: 0, schedule: runInMainThread ? IOScheduler.main : IOScheduler.scheduler)
                        .take(1).map { _ in () }
                }
            } else if let runInMainThread = instr.runInMainThread {
                mapSignal = runInMainThread ? mapSignal.deliverOnMainThread() : mapSignal.deliverOnSchedule()
            }
            if preSignal == nil {
                // sync to invoke instr
                instr.invoke(with: invocation, mode: mode, instrContext: instrContext)
                processFn(instr, bundleName, mode)
            } else {
                // async to invoke instr
                mapSignal = mapSignal.doNext { _ -> Void in
                    instr.invoke(with: invocation, mode: mode, instrContext: instrContext)
                    processFn(instr, bundleName, mode)
                }
                if let waitCallback = instr.waitCallback,
                   let subject = invocation.getWaitRACSubject(selector: waitCallback.0, blockor: waitCallback.1) {
                    mapSignal = mapSignal.ignoreAll().concat(subject.signal)
                }
            }
        }
        return mapSignal
    }

    // @result: (endPos, isBreaking => true、needAsyncInovoke => false、runToEnd => nil)
    private func findNormalInstructionEndPos(_ startPos: Int, _ endPos: Int,  _ mode: ScriptsInvokeMode,
                                             _ instructionList: [ScriptInstruction]) -> (Int, Bool?) {
        for i in startPos..<endPos {
            let instruction = instructionList[i]
            if mode == .manual && instruction.isStageCreateInstance {
                return (i, true)
            } else if mode == .semiAutomatic && !instruction.isStageCreateInstance && instruction.isBreakPoint {
                return (i - 1, true)
            } else if !instruction.isNormalInstruction {
                return (i, false)
            }
        }
        return (endPos - 1, nil)
    }
}
